今天刚学到作用域插槽的概念,结果跑到官网文档一看:
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的特性。
看到这里,突然有了一点想法。
为什么知识越学越多,因为学习的速度很难赶上技术迭代的速度,而且你越晚学习越会有这种感觉。Vue 3.0 的源码在前几天公布(国庆期间)了,根据群里大佬的说法,明年第一季度很可能就正式发布 3.0,似乎暗示着又一波学习热潮即将到来。工作以后和业务打交道,其实很少有时间可以学习了,所以现在 —— 大学的时间真的很宝贵,得好好珍惜。
为什么需要插槽?
我们先来看下这幅图:
可以看到,每个页面都有对应的 nav-bar
,所以这应该是一个可以复用的组件。问题是怎么复用呢?统一封装成一个组件肯定不行,因为这些 nav-bar
结构和内容并不完全相同;针对每个页面都封装一个组件也不行,因为它们有相同的结构和内容,这不利于复用。
所以我们需要的其实是一个足够灵活的组件,其内容可以更进一步地自定义,就像是积木一样,对于同一个位置 C,我们可以安插 A ,也可以安插 B。而插槽(slot) 就是用来实现这种灵活性的。
插槽是子组件暴露的一个让父组件传入自定义内容的接口。我们可以形象地把它理解成电脑的各种接口,提供了各种扩展。
单个插槽
// 子组件
<template>
<slot></slot>
</template>
// 父组件
<template>
<cpn>
<span>我是父组件传给子组件插槽的内容<span>
</cpn>
</template>
<slot></slot>
相当于一个空缺,等待父组件给它填充内容。当然也可以给 <slot></slot>
指定默认值,例如 <slot>我是默认值</slot>
,这种情况下,其将在父组件未传入内容时(即 <cpn></cpn>
)得到应用。
子组件可有多个 slot
,这些 slot
都将使用父组件传入的内容。
具名插槽
大部分时候,我们需要给特定的插槽传入特定的内容,所以每个插槽必须得有一个名字作为标识,这时候就要使用具名插槽了。具体来说,就是给 slot
添加 name
属性,之后在父组件中真正传入内容的时候,将内容包裹在有 slot="name"
属性的元素中。
假设我们要用同一个组件实现下面三种不同效果:
那么,首先会想到给这个子组件三个插槽,由于特定的插槽要传入特定的内容,所以我们这里使用具名插槽。代码如下:
<!--父组件模板-->
<div id="app">
<cpn>
<span slot="right">5</span>
</cpn>
<cpn>
<span slot="left">4</span>
<span slot="right">6</span>
</cpn>
<cpn>
<span slot="center">3</span>
<span slot="right">7</span>
</cpn>
</div>
<!--子组件模板-->
<template id="cpn">
<div>
<h2>我是子组件</h2>
<slot name="left">1</slot>
<slot name="center">2</slot>
<slot name="right">3</slot>
</div>
</template>
效果为:
注意:
Vue 2.6.0 之后改
slot
为v-slot
,且必须绑定在一个template
元素上:<span slot="right">5</span> <!--改为--> <template v-slot:right> <span>5</span> </template>
另外,我们可以将
v-slot:right
直接缩写为#right
,注意这仅适用于v-slot
有参数的情况,例如v-slot="xxx"
是不能缩写的。
编译作用域
关于编译作用域,只需要记住一条规则:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
来看一个例子:
<!--HTML-->
<div id="app">
<cpn v-if="isShow"></cpn>
</div>
// JS
const app = new Vue({
el:'#app',
components:{
cpn:{
template:"#cpn",
data(){
return {
isShow:true
}
}
}
},
data:{
isShow:false
}
})
对于这个例子,组件最后到底会不会显示(渲染)呢?
答案是不会。尽管父组件和子组件都有 isShow
这个变量,且后者为 true
,但子组件是存在于父级模板中的,其内容是在父级作用域中编译的,只能识别父级作用域中为 false
的那个 isShow
。
在这里,我们知道父模板无法直接访问子组件中的数据,但是有了作用域插槽之后,又不一样了。
作用域插槽
我们先来设想一种情况。假定子组件中有数据:
languages:["Java","C","Python","Swift"]
然后,现在要求在父组件中以不同的形式将这些数据进行展示,可能是列表,也可能是互相之间以斜杆分隔。如下图:
首先,数据的呈现方式不同,也即 HTML 结构不同,因此不能直接在子组件模板中书写结构,这时候想到了应该给子组件一个插槽,后面在父组件模板中再定义结构。但这样一来,父组件无可避免地要使用子组件的数据,而前面说过编译作用域的问题,所以这里的父组件实际上是无法拿到子组件数据的。怎么办呢?
这时候,作用域插槽就派上用场了。作用域插槽可以理解为是带数据的插槽,它允许我们在父组件模板中访问子组件的数据。典型的应用场景就是,数据在子组件中,但是渲染数据的工作必须由父组件完成。
实现分为两步:
- 将子组件数据绑定给
slot
上的属性(成为插槽 prop
) - 父组件模板中通过
slot-scope
拿到slot
对象(准确地说是包含所有插槽prop
的对象)并进行属性访问
以这道题为例:
// JS
const app = new Vue({
el:'#app',
components:{
cpn:{
template:"#cpn",
data(){
return {
languages:["Java","C","Python","Swift"]
}
}
}
}
})
首先,我们通过 :lang="languages"
将子组件的 languages
绑定到 slot
的 lang
属性中。
<!--子组件模板-->
<template id="cpn">
<div>
<h2>展示方式:</h2>
<slot :lang="languages"></slot>
</div>
</template>
接着,父组件模板中给插槽传入 template
,这个 template
带有 slot-scope="obj"
,使得我们可以通过 obj.lang
访问到子组件数据。
<div id="app">
<!--展示方式一-->
<cpn>
<template slot-scope="obj">
<ul>
<li v-for="item in obj.lang">{{item}}</li>
</ul>
</template>
</cpn>
<!--展示方式二-->
<cpn>
<template slot-scope="obj">
{{obj.lang.join('/')}}
</template>
</cpn>
<!--展示方式三-->
<cpn>
<template slot-scope="obj">
{{obj.lang.join('----')}}
</template>
</cpn>
</div>
最终实现我们想要的效果。
当然,关于作用域插槽还有一些地方需要注意:
如果数据过多,不可能一一绑定,这时候直接
v-bind
到一个属性就行,这个操作等同于手动绑定所有数据,方便了我们的访问。上例如果是<slot v-bind:obj></slot>
,后面直接obj.languages
就能拿到数据了Vue 2.6.0 之后,改
slot-scope
为v-slot
,对于像上面一样的匿名插槽,只需要使用v-slot="obj"
;对于具名插槽,则使用v-slot:name="obj"
。当然,v-slot:name
依然是表示具名插槽。正如前面所说,
v-slot:name="obj"
也可以缩写为#name="obj"
另外,还有解构插槽、动态插槽名等,具体可以看文档。
关于作用域插槽的应用,还有一个不错的案例,具体可以看下我之前翻译的一篇文章。我觉得里面有句话说得很有道理:
A good approach when you can’t understand something easily is to try put it to use in solving a problem.
大意是:当知识难以理解的时候,最好的办法就是拿它去解决问题。
我在这篇文章中其实也是尽量按照这个思路布局的,首先是刻意制造了一个问题,按照常规的思路没办法解决,接着引出相关概念,在实际情境中体会它的应用,这时候会有一种“噢,原来xxx可以解决这类型问题”的感觉,我觉得这或许是一种不错的学习方式,毕竟很多东西是为了解决问题而存在的,若能从最初问题产生的源头开始思考,兴许我们可以更好地理解它。